﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading;
using MugenInjection.Attributes;
using MugenInjection.Bindings;
using MugenInjection.Core;
using MugenInjection.Core.Components;
using MugenInjection.Delegates;
using MugenInjection.Exceptions;
using MugenInjection.Interface;
using MugenInjection.Interface.Components;

#if !NOEXPRESSION
using System.Linq.Expressions;
using MugenInjection.Parameters;
#endif

namespace MugenInjection.Infrastructure
{
    internal static class InjectorUtilsInternal
    {
        #region Fields

        internal static readonly IDictionary<string, object> ReadOnlyEmptyDictionary =
            new ReadOnlyEmtpyDictionary<string, object>();

        internal static readonly IList<IInjectionParameter> EmptyInjectionParams = new IInjectionParameter[0];

        internal static readonly object[] EmptyParams = new object[0];

        internal static readonly IComponent[] EmptyComponents = new IComponent[0];


        private static readonly CacheCollection<MemberInfo, ParameterInfo[]> CachedParameters =
                    new CacheCollection<MemberInfo, ParameterInfo[]>(50);

#if NETFX_CORE
        private static readonly CacheCollection<MemberInfo, IEnumerable<Attribute>> CachedAttributes =
            new CacheCollection<MemberInfo, IEnumerable<Attribute>>(50);
#else
        private static readonly CacheCollection<ICustomAttributeProvider, object[]> CachedAttributes = new CacheCollection<ICustomAttributeProvider, object[]>(50);
#endif

        #endregion

        #region Work with parameters

        internal static ParameterInfo[] GetParameterInfo(MethodBase methodBase)
        {
            lock (CachedParameters)
            {
                var cachedParameter = CachedParameters[methodBase];
                if (cachedParameter == null)
                {
                    cachedParameter = methodBase.GetParameters();
                    CachedParameters[methodBase] = cachedParameter;
                }
                return cachedParameter;
            }
        }

#if NETFX_CORE

        private static IEnumerable<Attribute> GetAttributes(MemberInfo memberInfo)
        {
            lock (CachedAttributes)
            {
                var attributes = CachedAttributes[memberInfo];
                if (attributes == null)
                {
                    attributes = memberInfo.GetCustomAttributes(typeof(Attribute), true);
                    CachedAttributes[memberInfo] = attributes;
                }
                return attributes;
            }
        }

        internal static T GetAttribute<T>(MemberInfo memberInfo) where T : Attribute
        {
            foreach (var attribute in GetAttributes(memberInfo))
            {
                var result = attribute as T;
                if (result == null) continue;
                return result;
            }
            return null;
        }

#else
        private static object[] GetAttributes(ICustomAttributeProvider memberInfo)
        {
            lock (CachedAttributes)
            {
                var attributes = CachedAttributes[memberInfo];
                if (attributes == null)
                {
                    attributes = memberInfo.GetCustomAttributes(typeof(Attribute), true);
                    CachedAttributes[memberInfo] = attributes;
                }
                return attributes;
            }
        }

        internal static T GetAttribute<T>(ICustomAttributeProvider memberInfo) where T : Attribute
        {
            var attributes = GetAttributes(memberInfo);
            for (int i = 0; i < attributes.Length; i++)
            {
                var customAttribute = attributes[i] as T;
                if (customAttribute == null) continue;
                return customAttribute;
            }
            return null;
        }
#endif

        /// <summary>
        /// Casts or creates the <see cref="IInjectionParameter"/>s.
        /// </summary>
        /// <param name="parameters">The specified <see cref="IInjectionParameter"/>s</param>
        /// <returns>An instances of <see cref="IInjectionParameter"/>.</returns>
        internal static IList<IInjectionParameter> GetParameters(IEnumerable<IInjectionParameter> parameters)
        {
            var injectionParameters = parameters as IList<IInjectionParameter>;
            if (injectionParameters == null)
                return new List<IInjectionParameter>(parameters);
            return injectionParameters;
        }

        internal static IDictionary<string, object> GetDictionary(IBindingContext bindingContext)
        {
            IDictionary<string, object> dictionary;
            if (bindingContext.SpecialParameters == null)
            {
                dictionary = new Dictionary<string, object>();
                SetDictionary(bindingContext, dictionary);
            }
            else if (bindingContext.SpecialParameters.IsReadOnly)
            {
                dictionary = new Dictionary<string, object>(bindingContext.SpecialParameters);
                SetDictionary(bindingContext, dictionary);
            }
            else
                dictionary = bindingContext.SpecialParameters;
            return dictionary;
        }

        private static void SetDictionary(IBindingContext context, IDictionary<string, object> dictionary)
        {
            var bindingContext = context as BindingContext;
            if (bindingContext != null)
            {
                bindingContext.SpecialParameters = dictionary;
                return;
            }
            context.Update(context.Service, context.Member, context.ParameterInfo, context.Injector, context.CallInjector, context.Parameters, dictionary);
        }

        #endregion

        #region Methods

        internal static bool HasFlag(ServiceType variable, ServiceType value)
        {
            return (variable & value) == value;
        }

        internal static string GetBuildPath(IBindingContext context)
        {
            var list = new List<IBindingContext>();
            while (context != null)
            {
                list.Insert(0, context);
                context = context.ParentBindingContext;
            }
            var sb = new StringBuilder();
            for (int index = 0; index < list.Count; index++)
            {
                var bindingContext = list[index];
                sb.AppendLine(String.Format("{0}) {1}\t", index + 1, FormatContext(bindingContext)));
            }
            return sb.ToString();
        }

        internal static string FormatContext(IBindingContext context)
        {
            var sb = new StringBuilder();
            sb.Append(context.ParentBindingContext == null
                          ? String.Format("Request of {0} ", FormatType(context.Service))
                          : String.Format("Injection of dependency {0} ", FormatType(context.Service)));

            if (context.SpecialParameters != null &&
                context.SpecialParameters.ContainsKey(SpecialParameterKeys.KeyedBindingParameter))
                sb.Append(string.Format("with keyed parameter \"{0}\" ",
                                        context.SpecialParameters[SpecialParameterKeys.KeyedBindingParameter] ??
                                        string.Empty));

            var formatMember = FormatMember(context.Member, context.ParameterInfo);
            if (!string.IsNullOrEmpty(formatMember))
                sb.Append(string.Format("in {0} ", formatMember));

            var formatBinding = FormatBinding(context.Binding);
            if (!string.IsNullOrEmpty(formatBinding))
                sb.Append(string.Format("using {0}", formatBinding));
            return sb.ToString();
        }

        internal static string FormatMember(MemberInfo member, ParameterInfo parameter)
        {
            if (member == null)
                return string.Empty;
            var sb = new StringBuilder();
#if NETFX_CORE
            switch (member.GetMemberType())
#else
            switch (member.MemberType)
#endif
            {
                case MemberTypes.Constructor:
                    if (parameter != null)
                        sb.Append(string.Format("parameter \"{0}\" of constructor \"{1}\" declared in {2} ", parameter, member,
                                                FormatType(member.DeclaringType)));
                    break;
                case MemberTypes.Field:
                    sb.Append(string.Format("parameter of field \"{0}\" declared in {1} ", member,
                                                FormatType(member.DeclaringType)));
                    break;
                case MemberTypes.Method:
                    if (parameter != null)
                        sb.Append(string.Format("parameter \"{0}\" of method \"{1}\" declared in {2} ", parameter, member,
                                                FormatType(member.DeclaringType)));
                    break;
                case MemberTypes.Property:
                    sb.Append(string.Format("parameter of property \"{0}\" declared in {1} ", member,
                                                FormatType(member.DeclaringType)));
                    break;
            }
            if (sb.Length == 0)
                sb.Append(string.Format("parameter \"{0}\" declared in {1}", member, FormatType(member.DeclaringType)));
            return sb.ToString();
        }

        internal static string FormatBinding(IBinding binding)
        {
            if (binding == null)
                return string.Empty;
            var sb = new StringBuilder();
            var formatTypes = FormatTypes(binding.Services);
            var typeBinding = binding as TypeBinding;
            if (typeBinding != null)
            {
                sb.Append(typeBinding.IsSelfBindable ? "the self bindable type binding " : "the type binding ");
                sb.Append(string.Format("from {0} to {1} ", formatTypes, FormatType(typeBinding.TypeTo)));
            }
            if (binding is ConstantBinding)
                sb.Append(string.Format("the constant binding from {0} to const value ", formatTypes));
            if (binding is MethodBinding)
                sb.Append(string.Format("the method binding from {0} to method ", formatTypes));
            var customBinding = binding as CustomBinding;
            if (customBinding != null)
                sb.Append(string.Format("the custom binding from {0} to {1} ", formatTypes,
                                        FormatBinding(customBinding.Binding)));
            if (sb.Length == 0)
                sb.Append(string.Format("the binding {0} from {1} ", binding, formatTypes));
            if (binding.ScopeLifecycle != null)
                sb.Append(string.Format("in {0} lifecycle.", FormatType(binding.ScopeLifecycle.GetType())));
            return sb.ToString();
        }

        internal static string FormatTypes(IList<Type> types)
        {
            if (types.Count == 0)
                return String.Empty;
            var stBuilder = new StringBuilder(types.Count);
            foreach (Type type in types)
                stBuilder.Append(String.Format("{0}, ", FormatType(type)));
            return stBuilder.ToString();
        }

        internal static string FormatType(Type type)
        {
            if (type == null)
                return "\"???\"";
            var name = type.FullName ?? type.Name;

            var firstB = name.IndexOf('[');
            if (firstB > 0)
                name = name.Substring(0, firstB);

            var firstC = name.IndexOf(',');
            if (firstC > 0)
                name = name.Substring(0, firstC);

            // remove namespace
            var lastPeriod = name.LastIndexOf('.');
            if (lastPeriod >= 0)
                name = name.Substring(lastPeriod + 1);

            if (name.Contains("AnonymousType"))
                return "\"AnonymousType\"";
            switch (name.ToLower())
            {
                case "int16": return "\"short\"";
                case "int32": return "\"int\"";
                case "int64": return "\"long\"";
                case "string": return "\"string\"";
                case "object": return "\"object\"";
                case "boolean": return "\"bool\"";
                case "void": return "\"void\"";
                case "char": return "\"char\"";
                case "byte": return "\"byte\"";
                case "uint16": return "\"ushort\"";
                case "uint32": return "\"uint\"";
                case "uint64": return "\"ulong\"";
                case "sbyte": return "\"sbyte\"";
                case "single": return "\"float\"";
                case "double": return "\"double\"";
                case "decimal": return "\"decimal\"";
            }

            var genericArguments = type.GetGenericArguments();
            if (genericArguments.Length > 0)
                name = FormatGenericType(name, genericArguments);

            return string.Format("\"{0}\"", name);
        }

        private static string FormatGenericType(string friendlyName, Type[] genericArguments)
        {
            var sb = new StringBuilder(friendlyName.Length + 10);
            var genericArgumentIndex = 0;
            var startIndex = 0;
            for (var index = 0; index < friendlyName.Length; index++)
            {
                if (friendlyName[index] == '`')
                {
                    var numArguments = friendlyName[index + 1] - 48;

                    sb.Append(friendlyName.Substring(startIndex, index - startIndex));
                    AppendGenericArguments(sb, genericArguments, genericArgumentIndex, numArguments);
                    genericArgumentIndex += numArguments;

                    startIndex = index + 2;
                }
            }
            if (startIndex < friendlyName.Length)
                sb.Append(friendlyName.Substring(startIndex));

            return sb.ToString();
        }

        private static void AppendGenericArguments(StringBuilder sb, Type[] genericArguments, int start, int count)
        {
            sb.Append("{");
            for (int i = 0; i < count; i++)
            {
                if (i != 0)
                    sb.Append(", ");
                sb.Append(FormatType(genericArguments[start + i]));
            }
            sb.Append("}");
        }



        internal static void ActivateInitializable(IBinding binding, IBindingContext bindingContext, ref object instance)
        {
            var initializable = instance as IInitializable;
            if (initializable == null) return;
            initializable.Initialize();
        }

        internal static void ActivateStartable(IBinding binding, IBindingContext bindingContext, ref object instance)
        {
            var startable = instance as IStartable;
            if (startable == null) return;
            startable.Start();
        }

        internal static void DeactivateStartable(object instance, bool dispose)
        {
            var startable = instance as IStartable;
            if (startable == null) return;
            startable.Stop();
        }

        internal static ConverterContext GetConverterContext(object inputValue)
        {
            var converterContext = inputValue as ConverterContext;
            if (converterContext != null)
                return converterContext;
            throw new NullReferenceException("The type ServiceType.Complex or ServiceType.SimpleWithActivation required a binding context.");
        }

        internal static bool IsEqualGenericDefinition(Type type, Type typeToCheck)
        {
#if !NETFX_CORE
            return type.IsGenericType && type.GetGenericTypeDefinition() == typeToCheck;
#else
            return type.GetTypeInfo().IsGenericType && type.GetGenericTypeDefinition() == typeToCheck;
#endif

        }

#if NETFX_CORE
        /// <summary>
        /// Indicates that type is self bindable.
        /// </summary>
        internal static bool IsSelfBindable(Type service)
        {
            var typeInfo = service.GetTypeInfo();
            return !typeInfo.IsInterface
                   && (!typeInfo.IsValueType || (typeInfo.IsValueType && System.Linq.Enumerable.Any(typeInfo.DeclaredConstructors)))
                   && service != typeof(string)
                   && !typeInfo.IsAbstract
                   && !typeInfo.ContainsGenericParameters;
        }

        /// <summary>
        /// Indicates that types is bindable.
        /// </summary>
        internal static bool IsBindable(Type typeFrom, Type service)
        {
            var serviceInfo = service.GetTypeInfo();
            var typeFromInfo = typeFrom.GetTypeInfo();

            if (typeFromInfo.ContainsGenericParameters && serviceInfo.ContainsGenericParameters)
            {
                return !serviceInfo.IsInterface
                       && (!serviceInfo.IsValueType || (serviceInfo.IsValueType && System.Linq.Enumerable.Any(serviceInfo.DeclaredConstructors)))
                       && service != typeof(string)
                       && !serviceInfo.IsAbstract;
            }
            Validate.IsAssignable(typeFrom, service);
            return !serviceInfo.IsInterface
                   && (!serviceInfo.IsValueType || (serviceInfo.IsValueType && System.Linq.Enumerable.Any(serviceInfo.DeclaredConstructors)))
                   && service != typeof(string)
                   && !serviceInfo.IsAbstract
                   & !serviceInfo.ContainsGenericParameters;
        }
#else
        /// <summary>
        /// Indicates that type is self bindable.
        /// </summary>
        internal static bool IsSelfBindable(Type service)
        {
            return !service.IsInterface
                   && (!service.IsValueType || (service.IsValueType && service.GetConstructors().Length > 0))
                   && service != typeof(string)
                   && !service.IsAbstract
                   && !service.ContainsGenericParameters;
        }

        /// <summary>
        /// Indicates that types is bindable.
        /// </summary>
        internal static bool IsBindable(Type typeFrom, Type typeTo)
        {
            if (typeFrom.ContainsGenericParameters && typeTo.ContainsGenericParameters)
            {
                return !typeTo.IsInterface
                       && (!typeTo.IsValueType || (typeTo.IsValueType && typeTo.GetConstructors().Length > 0))
                       && typeTo != typeof(string)
                       && !typeTo.IsAbstract;
            }
            Validate.IsAssignable(typeFrom, typeTo);
            return !typeTo.IsInterface
                   && (!typeTo.IsValueType || (typeTo.IsValueType && typeTo.GetConstructors().Length > 0))
                   && typeTo != typeof(string)
                   && !typeTo.IsAbstract
                   & !typeTo.ContainsGenericParameters;
        }
#endif

        internal static int Count(IEnumerable enumerable)
        {
            var list = enumerable as ICollection;
            if (list != null)
                return list.Count;
            int count = 0;
            var e = enumerable.GetEnumerator();
            while (e.MoveNext()) count++;
            return count;
        }

        internal static IEnumerable<T> Cast<T>(IEnumerable enumerable)
        {
#if NET2
            if (enumerable == null)
                yield break;
            foreach (object obj in enumerable)
                yield return (T)obj;
#else
            return System.Linq.Enumerable.Cast<T>(enumerable);
#endif

        }

        internal static IEnumerable<T> Concat<T>(IEnumerable<T> enumerable1, IEnumerable<T> enumerable2)
        {
#if NET2
            foreach (var obj in enumerable1)
                yield return obj;
            foreach (var obj in enumerable2)
                yield return obj;
#else
            return System.Linq.Enumerable.Concat<T>(enumerable1, enumerable2);
#endif

        }

        internal static IEnumerable<TResult> SelectMany<T, TResult>(IEnumerable<T> source, Func<T, IEnumerable<TResult>> selector)
        {
#if NET2
            foreach (var t in source)
            {
                foreach (var item in selector(t))
                    yield return item;
            }
#else
            return System.Linq.Enumerable.SelectMany(source, selector);
#endif
        }



        internal static bool RemoveDuplicate(ref IList<Type> types)
        {
            if (types.Count == 0 || types.Count == 1)
                return false;
            if (types.IsReadOnly)
                types = new List<Type>(types);
            bool result = false;
            var dictionary = new Dictionary<Type, bool>();
            for (int index = 0; index < types.Count; index++)
            {
                var binding = types[index];
                if (dictionary.ContainsKey(binding))
                {
                    types.Remove(binding);
                    index--;
                    result = true;
                    continue;
                }
                dictionary[binding] = true;
            }
            return result;
        }

        #endregion

        #region Work with expression

#if !NOEXPRESSION

        /// <summary>
        /// Gets the property represented by the lambda expression.
        /// </summary>
        /// <exception cref="T:System.ArgumentNullException">The <paramref name="property"/> is null.</exception><exception cref="T:System.ArgumentException">The <paramref name="property"/> is not a lambda expression or it does not represent a property access.</exception>
        internal static PropertyInfo GetProperty<T, TValue>(Expression<Func<T, TValue>> property)
        {
            var propertyInfo = ParseMemberExpression(property) as PropertyInfo;
            if (propertyInfo == null)
                throw new ArgumentException("Member is not a property");
            return propertyInfo;
        }

        internal static MethodCallExpression ParseMethodExpression(Expression expression)
        {
            Validate.ArgumentNotNull(expression, "expression");
            var lambdaExpression = expression as LambdaExpression;
            if (lambdaExpression == null)
                throw new ArgumentException("Not a lambda expression", "expression");
            if (lambdaExpression.Body.NodeType != ExpressionType.Call)
                throw new ArgumentException("Not a method call", "expression");
            return ((MethodCallExpression)lambdaExpression.Body);
        }

        internal static IList<IInjectionParameter> ParseMethodExpression<T>(Expression<Action<ResolveContext, T>> methodCall)
        {
            var parameters = new List<IInjectionParameter>();
            MethodCallExpression methodCallExpression = ParseMethodExpression(expression: methodCall);
            AddMethodParameters(methodCallExpression, parameters);
            return parameters;
        }

        internal static List<IInjectionParameter> ParseNewExpression<T>(Expression<Func<ResolveContext, T>> expression,
                                                                 out ConstructorInfo constructorInfo)
        {
            Validate.ArgumentNotNull(expression, "expression");
            var listParams = new List<IInjectionParameter>();
            switch (expression.Body.NodeType)
            {
                case ExpressionType.MemberInit:
                    var memberInitExpression = (MemberInitExpression)expression.Body;
                    AddMemberParameters(memberInitExpression, listParams);
                    constructorInfo = memberInitExpression.NewExpression.Constructor;
                    AddConstructorParameters(memberInitExpression.NewExpression, listParams);
                    break;
                case ExpressionType.New:
                    var newExpression = (NewExpression)expression.Body;
                    constructorInfo = newExpression.Constructor;
                    AddConstructorParameters(newExpression, listParams);
                    break;

                default:
                    throw new InvalidOperationException();
            }
            return listParams;
        }

        internal static NewExpression ParseNewExpression(Expression expression)
        {
            Validate.ArgumentNotNull(expression, "expression");
            var lambdaExpression = expression as LambdaExpression;
            if (lambdaExpression == null)
                throw new ArgumentException("Not a lambda expression", "expression");
            NewExpression newExpression = null;
            switch (lambdaExpression.Body.NodeType)
            {
                case ExpressionType.Convert:
                    newExpression = ((UnaryExpression)lambdaExpression.Body).Operand as NewExpression;
                    break;
                case ExpressionType.New:
                    newExpression = lambdaExpression.Body as NewExpression;
                    break;
            }
            if (newExpression == null)
                throw new ArgumentException("Not a new expression", "expression");
            return newExpression;
        }

        /// <summary>
        /// Returns a MemberInfo for an expression containing a call to a property.
        /// </summary>
        private static MemberInfo ParseMemberExpression<T, TValue>(Expression<Func<T, TValue>> expression)
        {
            Validate.ArgumentNotNull(expression, "expression");
            var lambdaExpression = expression as LambdaExpression;
            if (lambdaExpression == null)
                throw new ArgumentException("Not a lambda expression", "expression");
            MemberExpression memberExpression = null;
            switch (lambdaExpression.Body.NodeType)
            {
                case ExpressionType.Convert:
                    memberExpression = ((UnaryExpression)lambdaExpression.Body).Operand as MemberExpression;
                    break;
                case ExpressionType.MemberAccess:
                    memberExpression = lambdaExpression.Body as MemberExpression;
                    break;
            }
            if (memberExpression == null)
                throw new ArgumentException("Not a member access", "expression");

            if (memberExpression.Expression is ParameterExpression)
                return memberExpression.Member;
            throw new ArgumentException("Argument passed contains more than a single dot which is not allowed: " + expression, "expression");
        }

        private static void AddMemberParameters(MemberInitExpression memberInitExpression,
                                                  IList<IInjectionParameter> parameterBases)
        {
            foreach (MemberBinding memberBinding in memberInitExpression.Bindings)
            {
                var memberAssignment = (MemberAssignment)memberBinding;
                if (memberAssignment.Expression.NodeType == ExpressionType.Call)
                {
                    var methodCallExpression = (MethodCallExpression)memberAssignment.Expression;
                    if (methodCallExpression.Method.DeclaringType == typeof(ResolveContext)) continue;
                }
                Delegate compile = Expression.Lambda(memberAssignment.Expression).Compile();
                object value = compile.DynamicInvoke();
#if NETFX_CORE
                MemberTypes memberType = memberBinding.Member.GetMemberType();
#else
                MemberTypes memberType = memberBinding.Member.MemberType;
#endif
                switch (memberType)
                {
                    case MemberTypes.Property:
                        parameterBases.Add(new PropertyParameter(memberBinding.Member.Name,
                                                                 ((PropertyInfo)memberBinding.Member).PropertyType,
                                                                 value));
                        break;
                    default:
                        throw Validate.EnumOutOfRange(memberType, "memberType");
                }
            }
        }

        private static void AddConstructorParameters(NewExpression newExpression,
                                                     IList<IInjectionParameter> parameters)
        {
            ParameterInfo[] parameterInfos = newExpression.Constructor.GetParameters();
            for (int index = 0; index < newExpression.Arguments.Count; index++)
            {
                Expression argument = newExpression.Arguments[index];
                if (argument.NodeType == ExpressionType.Call)
                {
                    var methodCallExpression = (MethodCallExpression)argument;
                    if (methodCallExpression.Method.DeclaringType == typeof(ResolveContext)) continue;
                }
                Delegate compile = Expression.Lambda(argument).Compile();
                object value = compile.DynamicInvoke();
                parameters.Add(new ConstructorParameter(parameterInfos[index].Name, parameterInfos[index].ParameterType, value));
            }
        }

        private static void AddMethodParameters(MethodCallExpression methodCall, IList<IInjectionParameter> parameters)
        {
            ParameterInfo[] parameterInfos = methodCall.Method.GetParameters();
            for (int index = 0; index < methodCall.Arguments.Count; index++)
            {
                Expression argument = methodCall.Arguments[index];
                if (argument.NodeType == ExpressionType.Call)
                {
                    var methodCallExpression = (MethodCallExpression)argument;
                    if (methodCallExpression.Method.DeclaringType == typeof(ResolveContext)) continue;
                }
                Delegate compile = Expression.Lambda(argument).Compile();
                object value = compile.DynamicInvoke();
                parameters.Add(new MethodParameter(methodCall.Method.Name, parameterInfos[index].Name,
                                                   parameterInfos[index].ParameterType, value));
            }
        }

#endif

        #endregion
    }
}